/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package piccat.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import piccat.model.Side.PictureAddedEvent;
import piccat.model.Side.PictureDroppedEvent;
import piccat.model.Side.PicturePickupEvent;
import piccat.model.Side.ReadinessChangedEvent;
import piccat.model.Side.SideChangeListener;

/**
 * Top level class for holding an manipulating the game state. It contains
 * one or more "sides" - one for each player - the number of rows and columns
 * to be filled in each side, and a queue of pictures to be presented one at a
 * time.
 *
 * @author Hamish Morgan
 */
public final class Game {

    private static final boolean DEFAULT_CHECKING_VALIDITY = true;
    private static final boolean DEFAULT_CHECKING_COMPLETENESS = true;
    private static final boolean DEFAULT_CHECKING_MATCHING = true;
    private static final int MAX_PLAYERS = 8;
    //

    public enum State {

        INITIALIZING,
        WAIT_FOR_COMPLETENESS,
        WAIT_FOR_READY,
        WAIT_FOR_VALIDITY,
        WAIT_FOR_MATCHING,
        GIVE_PICTURE,
        GAME_OVER;
    }
    //
    protected final Logger userLog = LoggerFactory.getLogger("piccat.userLog");
    //
    private State state = State.INITIALIZING;
    /**
     * Pictures that will be provided to the players one at a time.
     */
    protected final Queue<Picture> pictureQueue;
    /**
     * Whether or not to check that the pictures are correctly organised
     * into columns before giving new pictures. Default: false.
     */
    private boolean checkValidity;
    /**
     * Wether or not to check that each player has positioned all available
     * pictures before providing new ones. Unpositioned pictures are those
     * which are either still in the initial location or are "floating".
     * Default: true
     */
    private boolean checkCompleteness;
    /**
     * Hold all listeners to this class
     */
    private final EventListenerList listenerList;
    /**
     * The change event is always the same, so create it lazily and cache it
     * for later use.
     */
    private ChangeEvent changeEvent = null;
    //
    private final List<Side> sides;
    /**
     * Whether or not to check that each players solution matches the other
     * before giving new pictures. Default: true.
     */
    private boolean checkMatching;

    /**
     * Create a new instance of the PicCat <tt>Game</tt> model.
     *
     * @param numPlayers    How many sides to have. (Between 1 and 8.)
     * @param rows          Number of rows in each grid. (At least 1.)
     * @param cols          Number of columns in each grid. (At least 1.)
     * @throws IllegalArgumentException If numPlayers is less than 1 or
     *      greater that 8. If rows is less than 1. Or if cols is less that 1.
     */
    public Game(int numPlayers, int rows, int cols) throws
            IllegalArgumentException {
        userLog.info("Creating {} player Game with {} rows and {} columns.",
                new Object[]{numPlayers, rows, cols});

        // Sanity check the arguments
        if (numPlayers < 1 || numPlayers > MAX_PLAYERS) {
            throw new IllegalArgumentException(
                    "The must be between 1 and " + MAX_PLAYERS +
                    " players, but found " + numPlayers + " players.");
        }
        if (cols < 1) {
            throw new IllegalArgumentException(
                    "There must be at least 1 columns, but found " + cols +
                    " cols");
        }
        if (rows < 1) {
            throw new IllegalArgumentException(
                    "There must be at least 1 row, but found " + rows +
                    " rows.");
        }

        pictureQueue = new LinkedList<Picture>();
        listenerList = new EventListenerList();
        checkCompleteness = DEFAULT_CHECKING_COMPLETENESS;
        checkValidity = DEFAULT_CHECKING_VALIDITY;
        checkMatching = DEFAULT_CHECKING_MATCHING;
        sides = new ArrayList<Side>(numPlayers);

        final SideChangeListener listener = new SideChangeHandler(this);
        for (int i = 0; i < numPlayers; i++) {
            Side side = new Side(rows, cols);
            side.setPlayerId(i);
            side.addChangeListener(listener);
            sides.add(side);
        }

        updateState();
    }

    //
    // ==================================================================
    // Configuration suff
    // ==================================================================
    // TODO: Possibly move this to a new GameConfig class
    //
    //
    public boolean isCheckingCompleteness() {
        return checkCompleteness;
    }

    public boolean isCheckingValidity() {
        return checkValidity;
    }

    public boolean isCheckingMatching() {
        return checkMatching;
    }

    public void setCheckCompleteness(boolean checkCompleteness) {
        if (this.checkCompleteness != checkCompleteness) {
            this.checkCompleteness = checkCompleteness;
            fireConfigChangedEvent();
        }
    }

    public void setCheckValidity(boolean checkValidity) {
        if (this.checkValidity != checkValidity) {
            this.checkValidity = checkValidity;
            fireConfigChangedEvent();
        }
    }

    public void setCheckMatching(boolean checkMatching) {
        if (this.checkMatching != checkMatching) {
            this.checkMatching = checkMatching;
            fireConfigChangedEvent();
        }
    }

    // ==================================================================
    protected void addPicture(Picture p) {
        pictureQueue.add(p);
        updateState();
    }

    protected void addAllPictures(Collection<Picture> pics) {
        for (Picture p : pics)
            pictureQueue.add(p);
        updateState();
    }

    public boolean isPicturesRemaining() {
        return !pictureQueue.isEmpty();
    }

    public void setReady(boolean ready) {
        for (Side side : sides)
            side.setReady(ready);
    }

    public boolean isReady() {
        for (Side side : sides)
            if (!side.isReady())
                return false;
        return true;
    }

    // ==================================================================
    public State getState() {
        return state;
    }

    protected void setState(State state) {
        if (this.state != state) {
            this.state = state;
            fireStateChangedEvent();
        }
    }

    public boolean isMatching() {
        if (!isCheckingMatching() || sides.size() < 2)
            return true;
        for (int i = 0; i < sides.size() - 1; i++) {
            if (!sides.get(i).isMatching(sides.get(i + 1)))
                return false;
        }
        return true;
    }

    public boolean isComplete() {
        if (!isCheckingCompleteness())
            return true;
        for (Side side : sides)
            if (!side.isComplete())
                return false;
        return true;
    }

    public boolean isValid() {
        if (!isCheckingValidity())
            return true;
        for (Side side : sides)
            if (!side.isValid())
                return false;
        return true;
    }

    // return true when all grid cells are full.
    public boolean isFinished() {
        for (Side side : sides)
            if (!side.isFinished())
                return false;
        return true;
    }

    public Set<Picture> getNonMatchingPictures() {
        final Set<Picture> incorrect = new HashSet<Picture>();
        if (isCheckingMatching()) {
            for (int i = 0; i < sides.size() - 1; i++) {
                Side sidei = sides.get(i);
                for (int j = i + 1; j < sides.size(); j++) {
                    Side sidej = sides.get(j);
                    incorrect.addAll(sidei.getNonMatchingPictures(sidej));
                    incorrect.addAll(sidej.getNonMatchingPictures(sidei));
                }
            }
        }
        return incorrect;
    }

    public Set<Picture> getIncompletePictures() {
        final Set<Picture> incorrect = new HashSet<Picture>();
        if (isCheckingCompleteness()) {
            for (Side side : sides)
                incorrect.addAll(side.getIncompletePictures());
        }
        return incorrect;
    }

    public Set<Picture> getInvalidPictures() {
        final Set<Picture> incorrect = new HashSet<Picture>();
        if (isCheckingValidity()) {
            for (Side side : sides)
                incorrect.addAll(side.getInvalidPictures());
        }
        return incorrect;
    }

    public Set<Picture> getIncorrectPictures() {
        final Set<Picture> incorrect = new HashSet<Picture>();

        if (isCheckingCompleteness())
            incorrect.addAll(getIncompletePictures());

        if (isCheckingValidity())
            incorrect.addAll(getInvalidPictures());

        if (isCheckingMatching())
            incorrect.addAll(getNonMatchingPictures());

        return incorrect;
    }

    public Set<Picture> getPics() {
        final Set<Picture> pics = new HashSet<Picture>();
        for (Side side : sides)
            pics.addAll(side.getPics());
        return pics;
    }

    public void updateState() {

        final State newState;

        if (!isComplete()) {
            newState = State.WAIT_FOR_COMPLETENESS;
        } else if (!isValid()) {
            newState = State.WAIT_FOR_VALIDITY;
        } else if (!isMatching()) {
            newState = State.WAIT_FOR_MATCHING;
        } else if (!isReady()) {
            newState = State.WAIT_FOR_READY;
        } else if (isPicturesRemaining()) {
            newState = State.GIVE_PICTURE;
        } else {
            newState = State.GAME_OVER;
        }

        final Set<Picture> incorrect = getIncorrectPictures();
        for (Picture pic : getPics())
            pic.setCorrect(!incorrect.contains(pic));

        if (newState != getState()) {
            if (newState != State.GIVE_PICTURE)
                setReady(false);
            setState(newState);
        }
    }

    public void doGivePicture() {
        if (pictureQueue.size() == 0)
            return;
        if(!isComplete() || !isMatching() || !isValid()) {
            return;
        }

        final Picture next = pictureQueue.poll();
        userLog.info("All players are ready, giving picture {}", next.getUrl());

        for (Side side : sides)
            side.addNextPic(next);
            
    }

    public int getNumSides() {
        return sides.size();
    }

    public Side getSide(int index) {
        return sides.get(index);
    }

    //
    // ==========================
    //
    public void addChangeListener(ChangeListener listener) {
        listenerList.add(ChangeListener.class, listener);
    }

    public void removeChangeListener(ChangeListener listener) {
        listenerList.remove(ChangeListener.class, listener);
    }

    public ChangeListener[] getChangeListeners() {
        return listenerList.getListeners(ChangeListener.class);
    }

    public int getChangeListenerCount() {
        return listenerList.getListenerCount(ChangeListener.class);
    }

    protected void fireConfigChangedEvent() {
        // TODO: Considering making this a different event
        fireStateChangedEvent();
    }

    protected void fireStateChangedEvent() {
        userLog.debug("Game state changed to {}.", getState().name());
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ChangeListener.class) {
                if (changeEvent == null)
                    changeEvent = new ChangeEvent(this);
                ((ChangeListener) listeners[i + 1]).stateChanged(changeEvent);
            }
        }
    }

    //========================================================
    /**
     * Class to recieve event notifications from each side. It's an inner
     * class so the external interface is less complicated.
     */
    protected static final class SideChangeHandler implements
            SideChangeListener {

        private final Game game;

        public SideChangeHandler(Game game) {
            this.game = game;
        }

        public void pictureAdded(PictureAddedEvent e) {
            game.updateState();
        }

        public void readinessChanged(ReadinessChangedEvent e) {
            game.updateState();
        }

        public void picturePickedUp(PicturePickupEvent e) {
            game.updateState();
        }

        public void pictureDropped(PictureDroppedEvent e) {
            game.updateState();
        }
    }
}
